package com.googlecode.archimedes.parser;

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.digester.Digester;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import com.googlecode.archimedes.filegen.model.AbstractMethod;
import com.googlecode.archimedes.filegen.model.AbstractType;
import com.googlecode.archimedes.filegen.model.AliasMember;
import com.googlecode.archimedes.filegen.model.Application;
import com.googlecode.archimedes.filegen.model.BeanPathMember;
import com.googlecode.archimedes.filegen.model.ConcreteMember;
import com.googlecode.archimedes.filegen.model.CustomStringMember;
import com.googlecode.archimedes.filegen.model.Entity;
import com.googlecode.archimedes.filegen.model.EnumMember;
import com.googlecode.archimedes.filegen.model.EnumeratedValue;
import com.googlecode.archimedes.filegen.model.Enumeration;
import com.googlecode.archimedes.filegen.model.InheritedRelationship;
import com.googlecode.archimedes.filegen.model.Interface;
import com.googlecode.archimedes.filegen.model.Member;
import com.googlecode.archimedes.filegen.model.ModelException;
import com.googlecode.archimedes.filegen.model.Relationship;
import com.googlecode.archimedes.filegen.model.Tab;
import com.googlecode.archimedes.filegen.model.validator.CustomValidator;
import com.googlecode.archimedes.filegen.model.validator.DoubleRangeValidator;
import com.googlecode.archimedes.filegen.model.validator.LengthRangeValidator;
import com.googlecode.archimedes.filegen.model.validator.LengthValidator;
import com.googlecode.archimedes.filegen.template.Template;
import com.googlecode.archimedes.util.ModelUtil;

/**
 * Handles parsing an XML file to create an Archimedes application. This class is used primarily by the ArchimedesTask class when run from within an
 * ANT build script.
 * 
 * @author Erik Gustavson
 * @version $Id: DataParser.java 2818 2006-08-22 18:23:59 +0000 (Tue, 22 Aug 2006) erik $
 */
public class DataParser implements ErrorHandler {

	private Log							log				= LogFactory.getLog(this.getClass());
	private Map<String, Entity>			entities		= new TreeMap<String, Entity>();
	private Map<String, Interface>		interfaces		= new TreeMap<String, Interface>();
	private Map<String, AbstractType>	allTypes		= new TreeMap<String, AbstractType>();
	private List<Template>				templates		= new ArrayList<Template>();
	private List<Tab>					tabs			= new ArrayList<Tab>();
	private Application					application		= null;
	private Map<String, Enumeration>	enumerations	= new TreeMap<String, Enumeration>();

	public void parse(File xmlFile, URL dtdURL) throws ParseException {
		if (xmlFile.exists() != true) {
			throw new ParseException("The xmlFile [" + xmlFile + "] does not exist.");
		}

		parseXML(xmlFile, dtdURL);
	}

	/**
	 * Parses the specified XML file.
	 */
	private void parseXML(File xmlFile, URL dtdURL) throws ParseException {
		try {

			log.debug("Configuring Archimdes Data Parser.");

			Digester digester = new Digester();

			// register the location of the DTD
			digester.register("-//Archimedes Codegen//DTD Archimedes 1.0//EN", dtdURL.toString());

			// set up Digester
			digester.push(this);
			digester.setLogger(log);
			digester.setValidating(true);
			digester.setErrorHandler(this);

			// look for entities, instantiate & configure them
			digester.addObjectCreate("archimedes/entities/entity", Entity.class);
			digester.addSetProperties("archimedes/entities/entity", "extends", "extendsReference");
			digester.addBeanPropertySetter("archimedes/entities/entity/description");

			// add the new biz object to our listof objects using the addEntity method
			digester.addSetNext("archimedes/entities/entity", "addEntity");

			// set interface references onto the entity
			digester.addCallMethod("archimedes/entities/entity/implements", "addInterfaceRef", 1, new String[] { "java.lang.String" });
			digester.addCallParam("archimedes/entities/entity/implements", 0, "name");

			// look for entities, instantiate & configure them
			digester.addObjectCreate("archimedes/entities/interface", Interface.class);
			digester.addSetProperties("archimedes/entities/interface");

			// add the new biz object to our listof objects using the addEntity method
			digester.addSetNext("archimedes/entities/interface", "addInterface");

			// look for enumerations, instatiate & configure them
			digester.addObjectCreate("archimedes/entities/enumeration", Enumeration.class);
			digester.addSetProperties("archimedes/entities/enumeration");
			digester.addBeanPropertySetter("archimedes/entities/enumeration/description");
			digester.addSetNext("archimedes/entities/enumeration", "addEnumeration");

			// look for enumeration values, instantiate, config & add them
			digester.addObjectCreate("*/enumeration/value", EnumeratedValue.class);
			digester.addSetProperties("*/enumeration/value");
			digester.addSetNext("*/enumeration/value", "addValue");

			// look for members, instantiate & configure them.
			digester.addObjectCreate("*/members/member", ConcreteMember.class);
			digester.addSetProperties("*/members/member", "default", "defaultValue");

			// look for virtual members, instantiate & configure them.
			digester.addObjectCreate("*/members/alias", AliasMember.class);
			digester.addSetProperties("*/members/alias");

			// look for virtual members, instantiate & configure them.
			digester.addObjectCreate("*/members/custom", CustomStringMember.class);
			digester.addSetProperties("*/members/custom");

			// look for UI-Only members, instantiate & configure them.
			digester.addObjectCreate("*/members/beanpath", BeanPathMember.class);
			digester.addSetProperties("*/members/beanpath");

			// look for ENUM members, instantiate & configure them.
			digester.addObjectCreate("*/members/enum", EnumMember.class);
			digester.addSetProperties("*/members/enum");

			// add the members to the biz object
			digester.addSetNext("*/members/member", "addMember");
			digester.addSetNext("*/members/alias", "addMember");
			digester.addSetNext("*/members/custom", "addMember");
			digester.addSetNext("*/members/beanpath", "addMember");
			digester.addSetNext("*/members/enum", "addMember");

			// add abstract methods
			digester.addObjectCreate("*/abstractMethod", AbstractMethod.class);
			digester.addSetProperties("*/abstractMethod");
			digester.addSetNext("*/abstractMethod", "addAbstractMethod");

			// look for validators, instantiate & configure them.
			digester.addObjectCreate("*/validator", CustomValidator.class);
			digester.addObjectCreate("*/rangeValidator", DoubleRangeValidator.class);
			digester.addObjectCreate("*/lengthValidator", LengthValidator.class);
			digester.addObjectCreate("*/lengthRangeValidator", LengthRangeValidator.class);

			digester.addSetProperties("*/validator");
			digester.addSetProperties("*/rangeValidator");
			digester.addSetProperties("*/lengthValidator");
			digester.addSetProperties("*/lengthRangeValidator");

			// add the Validators to the member object
			digester.addSetNext("*/validator", "addValidator");
			digester.addSetNext("*/rangeValidator", "addValidator");
			digester.addSetNext("*/lengthValidator", "addValidator");
			digester.addSetNext("*/lengthRangeValidator", "addValidator");

			// look for templates, instantiate & configure them
			digester.addObjectCreate("archimedes/templates/template", Template.class);
			digester.addSetProperties("archimedes/templates/template", "allowedAssetType", "allowedAssetTypeAsString");
			digester.addSetProperties("archimedes/templates/template");

			// add the templates to our list of templates
			digester.addSetNext("archimedes/templates/template", "addTemplate");

			// look at the relationships
			digester.addObjectCreate("*/relation", Relationship.class);
			digester.addSetProperties("*/relation");
			digester.addSetProperties("*/relation/logical", new String[] { "source", "target" }, new String[] { "aliasSource", "aliasTarget" });
			digester.addSetNext("*/relation", "addRelationship");

			// look at the relationships
			digester.addObjectCreate("*/inheritedrelation", InheritedRelationship.class);
			digester.addSetProperties("*/inheritedrelation");
			digester.addSetProperties("*/inheritedrelation/logical", new String[] { "source", "target" }, new String[] { "aliasSource", "aliasTarget" });
			digester.addSetNext("*/inheritedrelation", "addRelationship");

			digester.addObjectCreate("archimedes/application", Application.class);
			digester.addSetProperties("archimedes/application");
			digester.addSetNext("archimedes/application", "setApplication");

			// process the tabs
			digester.addObjectCreate("archimedes/tabs", ArrayList.class);
			digester.addObjectCreate("archimedes/tabs/tab", Tab.class);
			digester.addSetProperties("archimedes/tabs/tab");
			digester.addSetNext("archimedes/tabs/tab", "add");
			digester.addCallMethod("archimedes/tabs/tab/entity-ref", "addEntityRef", 1, new String[] { "java.lang.String" });
			digester.addCallParam("archimedes/tabs/tab/entity-ref", 0, "name");

			digester.addSetNext("archimedes/tabs", "addTabs");

			digester.parse(xmlFile);

			// run some validation routines here
			resolveInheritence();
			resolveInterfaces();
			resolveInheritedRelationships(); // has to happend before we can resolve the relationships.
			resolveRelationships();
			resolveEnumerations();

			// make sure everything looks good
			validateRelationships();

		} catch (Exception e) {
			throw new ParseException("There was a problem reading the file [" + xmlFile + "]", e);
		}
	}

	/**
	 * Loops over all the relationships and validates that they are correctly put together
	 */
	protected void validateRelationships() throws ParseException {
		for (Entity entity : getEntities().values()) {
			for (Relationship relation : entity.getRelationships(false)) { // DO NOT INCLUDE ENTITY'S INHERITED RELATIONSHIPS - SEE TPS-58

				switch (relation.getAssociation()) {

				case OneToMany:
					if (relation.isInternal()) {
						throw new ParseException("The OneToMany relationship [" + relation.getName() + "] should not be set to 'internal=true'");
					}
					break;

				case OneToOne:
					if (relation.isInternal()) {
						throw new ParseException("The OneToOne relationship [" + relation.getName() + "] should not be set to 'internal=true'");
					}
					if (relation instanceof InheritedRelationship) {
						throw new ParseException("InheritedRelationships are not supported on OneToOne relationships yet");
					}
					break;

				case ManyToMany:
					if (!relation.isInternal()) {
						throw new ParseException("The ManyToMany relationship [" + relation.getName() + "] should be 'internal=true'");
					}
					if (relation instanceof InheritedRelationship) {
						throw new ParseException("InheritedRelationships are not supported on OneToOne relationships yet");
					}
					break;

				case ManyToOne:
					if (!relation.isInternal()) {
						throw new ParseException("The ManyToOne relationship [" + relation.getName() + "] should be 'internal=true'");
					}
					if (relation instanceof InheritedRelationship) {
						throw new ParseException("InheritedRelationships are not supported on OneToOne relationships yet");
					}
					break;

				default:
					throw new ParseException("No relationship type was found for relation [" + relation.getName() + "].");
				}
			}
		}
	}

	/**
	 * Resolves all of the InheritedRelationships
	 */
	protected void resolveInheritedRelationships() throws ParseException {
		// make a map of all the real relationships we know about
		Map<String, Relationship> relMap = new HashMap<String, Relationship>();
		for (Entity entity : getEntities().values()) {
			for (Relationship relation : entity.getRelationships(false)) {
				if (relation instanceof InheritedRelationship == false) {
					relMap.put(relation.getName(), relation);
				}
			}
		}

		// resolve the parent relationships of each inherited relationship
		for (Entity entity : getEntities().values()) {
			for (Relationship relation : entity.getRelationships(false)) { // DO NOT INCLUDE ENTITY'S INHERITED RELATIONSHIPS - SEE TPS-58
				if (relation instanceof InheritedRelationship) { // skip over InheritedRelationships
					InheritedRelationship irelation = (InheritedRelationship) relation;

					if (irelation.getParentRelationship() == null) { // we need to configure this relationship
						if (relMap.containsKey(irelation.getParent())) {
							irelation.setParentRelationship(relMap.get(irelation.getParent()));
						} else {
							log.error("The parent [" + irelation.getParent() + "] of InheritedRelationship [" + irelation.getName() + "] does not exist.");
							throw new ParseException("The parent [" + irelation.getParent() + "] of InheritedRelationship [" + irelation.getName() + "] does not exist.");
						}
					}
				}
			}
		}
	}

	/**
	 * Resolves all of the enumerations on any enum members
	 */
	private void resolveEnumerations() {
		for (AbstractType type : this.allTypes.values()) {

			for (Member member : type.getMembers()) {
				if (member instanceof EnumMember) {
					EnumMember enumMember = (EnumMember) member;
					if (enumMember.getEnumeration() == null) {
						Enumeration anEnum = this.getEnumerations().get(enumMember.getEnumName());

						// throw exception if we can't find our real enum
						if (anEnum == null) {
							throw new RuntimeException("Cannot find the enum [" + enumMember.getEnumName() + "] referenced by member [" + member.getMemberName() + "] of entity ["
									+ type.getEntityName() + "].");
						}

						// set the enum onto the member for later usage
						enumMember.setEnumeration(anEnum);
					}
				}
			}
		}
	}

	/**
	 * Runs through each entity and resolves its relationship to the other entities. Assumption: The source entity already has a relationship in its
	 * set, but the relationship has not necessarily been configured properly nor has the target entity been linked to the relationship. Output: All
	 * entities with relationships have correctly linked references, such that the Target and Source entities on the relationship are the same
	 * instance as the Target and Source entities that enclose the Relationship instance.
	 */
	protected void resolveRelationships() throws ParseException, ModelException {
		// do the regular relationships first
		for (Entity entity : getEntities().values()) {
			for (Relationship relation : entity.getRelationships(false)) { // DO NOT INCLUDE ENTITY'S INHERITED RELATIONSHIPS - SEE TPS-58
				if (relation.getToEntity() == null) { // we need to configure this relationship
					if (relation instanceof InheritedRelationship == false) { // skip over InheritedRelationships
						resolveRelationship(entity, relation);
					}
				}
			}

		}

		// do the inherited relationships second
		for (Entity entity : getEntities().values()) {
			for (Relationship relation : entity.getRelationships(false)) { // DO NOT INCLUDE ENTITY'S INHERITED RELATIONSHIPS - SEE TPS-58
				if (relation.getToEntity() == null) { // we need to configure this relationship
					if (relation instanceof InheritedRelationship == true) { // process InheritedRelationships second
						resolveRelationship(entity, relation);
					}
				}
			}
		}
	}

	/**
	 * Resolves the particular relationship and entity combination passed in
	 * 
	 * @param entity
	 * @param relation
	 * @throws ParseException
	 * @throws ModelException
	 */
	private void resolveRelationship(Entity entity, Relationship relation) throws ParseException, ModelException {
		AbstractType source = entity;
		AbstractType target = findType(relation.getTarget());
		if (target == null) {
			throw new ParseException("The target entity [" + relation.getTarget() + "] cannot be resolved for relationship [" + relation.getName() + "].");
		}

		relation.setAssociation(ModelUtil.findAssociationType(relation.getType()));
		if (relation.getAssociation() == null) {
			throw new ParseException("Unknown relationship [" + relation.getType() + "]");
		}

		relation.setDirection(ModelUtil.findDirectionType(relation.getDir()));

		relation.setFromEntity(source);
		relation.setToEntity(target);
		relation.setInternalized(relation.isInternal()); // resolve internal -to-> internalized

		// set the relationship onto the Target entity. The source entity already has the relationship marked.
		target.addRelationship(relation);
	}

	/**
	 * Runs through all the entities that have any interface references, locate the interface object and resolve to a real interface.
	 */
	protected void resolveInterfaces() throws ParseException {
		for (Entity entity : getEntities().values()) {

			for (String ref : entity.getInterfaceRefs()) {

				Interface anInterface = interfaces.get(ref);
				if (anInterface == null) {
					throw new ParseException("Cannot find the named interface [" + ref + "] of entity [" + entity.getEntityName() + "]");
				}
				entity.addInterface(anInterface);
			}
		}
	}

	/**
	 * Runs through all the entities that have an extends attribute, validates and sets the inherited entity on the extending entity.
	 */
	protected void resolveInheritence() throws ParseException {
		for (Entity entity : getEntities().values()) {

			if (entity.getExtendsReference() != null) {
				Entity parent = getEntities().get(entity.getExtendsReference());
				if (parent == null) {
					throw new ParseException("Cannot find the named parent [" + entity.getExtendsReference() + "] of entity [" + entity.getEntityName() + "]");
				}

				entity.setExtendedEntity(parent);
			}
		}
	}

	/**
	 * Adds the entity to the data parser
	 * 
	 * @param entity
	 */
	public void addEntity(Entity entity) {
		entities.put(entity.getEntityName(), entity);
		allTypes.put(entity.getEntityName(), entity);
	}

	/**
	 * Adds the interface to the data parser
	 * 
	 * @param anInterface
	 */
	public void addInterface(Interface anInterface) {
		interfaces.put(anInterface.getEntityName(), anInterface);
		allTypes.put(anInterface.getEntityName(), anInterface);
	}

	/**
	 * Adds the Enumeration to the DataParser
	 * 
	 * @param anEnumeration
	 */
	public void addEnumeration(Enumeration anEnumeration) {
		enumerations.put(anEnumeration.getName(), anEnumeration);
	}

	public void addTemplate(Object object) {
		templates.add((Template) object);
	}

	/**
	 * Sets the tabs onto this data parser and in the process, resolves all entity references. Errors are thrown if there is a bad entity reference on
	 * the tab.
	 * 
	 * @param tabs
	 */
	public void addTabs(List<Tab> tabs) throws ParseException {
		for (Tab tab : tabs) {

			for (String entityRef : tab.getEntityRefs()) {
				Entity entity = getEntities().get(entityRef);
				if (entity == null) {
					throw new ParseException("Unable to find a entity by the name of [" + entityRef + "] for tab [" + tab.getName() + "]");
				}

				tab.addEntity(entity);
			}
		}

		this.tabs = tabs;
	}

	/**
	 * Look through the entities for a matching string type. Interfaces are not supported in persistable relationships (at the moment). TODO Allow
	 * Interfaces to participate in relationships.
	 * 
	 * @param type
	 * @return
	 */
	protected AbstractType findType(String type) {
		return entities.get(type);
	}

	// retrieval methods
	public Map<String, Entity> getEntities() {
		return this.entities;
	}

	public List<Template> getTemplates() {
		return templates;
	}

	public List<Tab> getTabs() {
		return this.tabs;
	}

	public Map<String, Interface> getInterfaces() {
		return this.interfaces;
	}

	public Map<String, Enumeration> getEnumerations() {
		return this.enumerations;
	}

	// error handling methods
	/**
	 * @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
	 */
	public void error(SAXParseException ex) throws SAXException {
		String error = String.format("There was a problem at line %s, column %s - SAX message is \"%s]\"", ex.getLineNumber(), ex.getColumnNumber(), ex.getMessage());
		log.error(error, ex);
		throw new RuntimeException(ex);
	}

	/**
	 * @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
	 */
	public void fatalError(SAXParseException ex) throws SAXException {
		error(ex);
	}

	/**
	 * @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
	 */
	public void warning(SAXParseException ex) throws SAXException {
		log.warn("Unable to parse XML.", ex);
	}

	/**
	 * Returns the application.
	 * 
	 * @return Application value of application
	 */
	public Application getApplication() {
		return application;
	}

	/**
	 * Sets the application
	 * 
	 * @param application
	 *            The application to set.
	 */
	public void setApplication(Application application) {
		this.application = application;
	}
}